package disjunction.core
{
	import disjunction.core.geom.Cuboid;
	import disjunction.core.geom.Vector2;
	import disjunction.core.geom.Vector3;
	import disjunction.extensions.math.MathUtils;
	
	import flash.geom.Rectangle;

	/**
	 * 2D Camera which provides configurable parallax, and the ability to change depth (z).
	 * Note that depth ordering *is* parallax. To have one object in front of another, increase
	 * its z by a very small margin, such that the parallax differential is unnoticeable.
	 * 
	 * To use no depth-ordering, the near and far depth planes can be set to zero
	 * (all entity model depths must also be set to zero).
	 * 
	 * Always faces along the +Z axis. Rotation therefore occurs about this axis.
	 * 
	 * @author Nick Wiggill
	 */
	
	
	//Parallax depth ordering is achieved through increasing z value. Only values between the camera's
	//near and far planes are rendered. 
	
	//There are two kinds of parallax:
	
	//FP-indexed parallax: Expensive, but a closer approximation of 3D (using billboarded entities of
	//course). Every unique depth index (in FP) has its own AABB created (or rather, a an existing
	//AABB instance is modified) and then entities in that depth have their AABBs checked against it.
	
	//First parallax layer cannot have zero distance, since this would mean there is no way to calc
	//the position of a point across the bounding volume (a divide by zero would occur).
	//so the camera must have some minimum focal length (easiest is to use 1)
	
	public class ParallaxCamera implements ICamera, ISpatial3D
	{
		private var _position:Vector3;
		private var _scale:Vector3;
		private var _rotation:Vector3;

		public var fov:Number = 60;

		//needs access to entire entity lists
		
		public var screenPlane:Rectangle = new Rectangle();
		//public var cameraViewDepth:Number;
		public var cameraRotation:Number = 0;

		/** The distance at which no depth-related translation is applied (zero for calc purposes). !<0 */
		public var focalDistance:Number;
		
		/** The current depth on which the focus lies. */
		private function get focalDepth():Number
		{
			return position.z + focalDistance;
		}
		
		/** The distance before which we cull. Relative to own focal depth. !< 0 */
		public var minDistance:Number = 0; 
		
		/** The distance beyond which we cull. Relative to own focal depth. !< 0*/
		public var maxDistance:Number = 5;
		
		/** Whether or not to apply parallax math at all. Off makes view orthogonal and more efficient. */
		public var useParallax:Boolean = true;
		
		public function set width(value:int):void
		{
			//_width = value;
			screenPlane.width = value;
			screenPlane.x = -value / 2;
		}
		
		public function set height(value:int):void
		{
			//_height = value;
			screenPlane.height = value;
			screenPlane.x = -value / 2;
		}
		
		public function update(entities:Array):void
		{
			screenPlane.x = position.x - screenPlane.width / 2;
			screenPlane.y = position.y - screenPlane.height / 2;
			
			//Note that this approach only processes entities if already found to be in
			//visible depth range (optimisation). It does final culling only after
			//intermediate transformations are applied. So there are 2 culling phases:
			//depth-based, and viewrect based.
			
			//Depth sort.
			//TODO Test whether this can actually work with a nested prop!!!
			entities.sortOn("model.z", Array.DESCENDING | Array.NUMERIC);
			
			//Cull by depth / Transform view based on spatial characteristics. (combined for optimisation)
			var entity:AbstractEntity2D;
			var model:ISpatial3D;
			for each (entity in entities)
			{
				model = entity.model as ISpatial3D;
				//transform
				if ( MathUtils.isInRange(model.position.z, minDistance, maxDistance ) )
				{
					entity.position.x = model.position.x;
					entity.position.y = model.position.y;
					entity.rotation = model.rotation.z;
					entity.scale.x = model.scale.x;
					entity.scale.y = model.scale.y;
				}
				//...or cull (for next step)
				else
					entities.splice(entities.lastIndexOf(entity), 1);
			}
			
			//Retransform view based on depth and camera centre.
			for each (entity in entities)
			{
				model = entity.model as ISpatial3D;
				
				if (model.position.z >= minDistance || model.position.z <= maxDistance)
				{
					var distanceDivisor:Number = focalDepth * Math.tan(fov/2); //O = A tan(theta)
					var distanceMultiplier:Number = 1/distanceDivisor;
					entity.scale = new Vector2(distanceDivisor, distanceDivisor);
					
					//TODO these need to be translated according to whatever the current centre
					//of the view is.
					//find distance from centre of view (in pos or neg)
					//divide by distanceDivisor = derivedDistance
					//set new pos as centre + deriveDistance
					
					var distanceFromCentre:Vector2 = entity.position.sub(position.toVector2());
					distanceFromCentre.mulLocal(distanceMultiplier);
					
					entity.position = position.toVector2().add(distanceFromCentre);
					//view.x = distance.x + viewCentre.x;//model.x / distanceDivisor;
					//view.y = distance.y + viewCentre.y;//model.y / distanceDivisor;
					
					entity.model.scale.mulLocal(distanceMultiplier);
					
					//Recull, removing what is not in the "frustum" volume. (In fact there is none,
					//but the effects of transforming the entities according to their distance
					//lends the effect of a frustum.)
					
					if (entity.bounds.intersects(screenPlane))
					{
						entities.splice(entities.lastIndexOf(entity), 1);
					}
				}				
			}
		}
		
		public function get position():Vector3
		{
			return _position;//.clone();
		}
		public function get scale():Vector3
		{
			return _scale;//.clone();
		}
		public function get rotation():Vector3
		{
			return _rotation;//.clone();
		}
		public function get bounds():Cuboid
		{
			//TODO implement?
			return null;
		}
		
		public function set position(value:Vector3):void
		{
			_position.x = value.x;
			_position.y = value.y;
			_position.z = value.z;
		}
		public function set scale(value:Vector3):void
		{
			_scale.x = value.x;
			_scale.y = value.y;
			_scale.z = value.z;
		}
		public function set rotation(value:Vector3):void
		{
			_rotation.x = value.x;
			_rotation.y = value.y;
			_rotation.z = value.z;
		}
	}
}